# -*- coding: utf-8 -*-
#
# Copyright (c) 2007 - 2014 -- Lars Heuer - Semagia <http://www.semagia.com/>.
# All rights reserved.
#
# BSD license.
#
"""\
This module provides a ``TopicMapWriter`` which writes 
`XML Topic Maps (XTM) 2.0 <http://www.isotopicmaps.org/sam/sam-xtm/2006-06-19/>`_
and `XML Topic Maps (XTM) 2.1 <http://www.isotopicmaps.org/sam/sam-xtm/2009-11-19/>`_

:author:       Lars Heuer (heuer[at]semagia.com)
:organization: Semagia - http://www.semagia.com/
:license:      BSD license
"""
import io
from xml.sax import make_parser
from xml.sax.xmlreader import InputSource
import xml.sax.handler as sax_handler
from tm.xmlutils import XMLWriter, xmlwriter_as_contenthandler
from mappa import XSD
from mappa._internal.it import one_of, no
from mappa._internal.utils import topic_id
from mappa.utils import is_default_name, is_default_name_type

from mappa import voc
_NS_XTM = voc.XTM
del voc


def _is_omitable(topic, sids, slos, iids):
    """\
    Returns if the `topic` has just one identity and no further 
    characteristics (occurrences, names).
    """
    # No need to check 'roles played' and 'reified' here since
    # the reified statement refers to the topic already and
    # the roles played are serialized through the associations
    return len(iids) + len(sids) + len(slos) <= 1 \
            and no(topic.names) \
            and no(topic.occurrences)


class XTM2TopicMapWriter(object):
    """\
    The XTM 2.0 / 2.1 writer.
    """
    def __init__(self, out, base, encoding='utf-8', version=None):
        if not out:
            raise TypeError('"out" is not specified')
        if version and version not in (2.0, 2.1):
            raise ValueError('Unexpected version number: "%s"' % str(version))
        if not base:
            raise TypeError('"base" is not specified')
        self._version = version or 2.0
        self._parser = None    # SAX parser used to serialize occurrence / variants values of datatype xs:anyType
        self._writer = None    # XMLWriter instance to serialize a topic map
        self._out = out
        self._encoding = encoding
        self._base = base
        self.export_iids = True
        self.prettify = False
        if self._version == 2.0:
            self._reifier = self._reifier_xtm20
            self._write_topic_ref = self._write_topic_ref_xtm20
            self._write_reifier = self._write_reifier_xtm20
        else:
            self._reifier = self._reifier_xtm21
            self._write_topic_ref = self._write_topic_ref_xtm21
            self._write_reifier = self._write_reifier_xtm21

    def write(self, topicmap):
        """\
        Serializes the specified ``topicmap``.
        """
        self._writer = XMLWriter(self._out, self._encoding)
        writer = self._writer
        writer.prettify = self.prettify
        writer.startDocument()
        writer.comment(u'Generated by Mappa - http://mappa.semagia.com/')
        attrs = {u'xmlns': _NS_XTM, u'version': u'%s' % unicode(self._version)}
        attrs.update(self._reifier(topicmap) or {})
        writer.startElement(u'topicMap', attrs)
        self._write_reifier(topicmap)
        self._write_iids(topicmap)
        write_topic = self._write_topic
        for topic in topicmap.topics:
            write_topic(topic)
        write_assoc = self._write_association
        for assoc in topicmap.associations:
            write_assoc(assoc)
        writer.endElement(u'topicMap')
        writer.endDocument()

    def _write_topic(self, topic):
        """\
        Serializes a topic and its characteristics.
        
        `topic`
            The topic to serialize
        """
        force_iids = False
        sids = tuple(topic.sids)
        slos = tuple(topic.slos)
        iids = tuple(topic.iids)
        if self._version == 2.0:
            if is_default_name_type(topic) and _is_omitable(topic, sids, slos, iids):
                return
            self._writer.startElement(u'topic', {u'id': topic_id(self._base, topic)})
        else:
            if not sids and not slos and not iids:
                self._writer.startElement(u'topic', {u'id': topic_id(self._base, topic)})
            else:
                self._writer.startElement(u'topic')
                force_iids = not (sids or slos)
        # item identifiers are written if export_iids is True or if the
        # writer is in XTM 2.1 mode and the topic has no subject identifiers 
        # or subject locators
        if self.export_iids or force_iids:
            # Cannot use _write_iids since it does nothing if export_iids is False
            self._write_locators(u'itemIdentity', iids)
        self._write_locators(u'subjectLocator', slos)
        self._write_locators(u'subjectIdentifier', sids)
        write_name = self._write_name
        for name in topic.names:
            write_name(name)
        write_occurrence = self._write_occurrence
        for occ in topic.occurrences:
            write_occurrence(occ)
        self._writer.endElement(u'topic')

    def _write_occurrence(self, occ):
        """\
        Serializes the occurrence.
        
        `occurrence`
            An occurrence.
        """
        self._writer.startElement(u'occurrence', self._reifier(occ))
        self._write_reifier(occ)
        self._write_iids(occ)
        self._write_type(occ)
        self._write_scope(occ)
        self._write_data(occ)
        self._writer.endElement(u'occurrence')

    def _write_name(self, name):
        """\
        Serializes the topic name and its variants.
        
        `name`
            A name.
        """
        self._writer.startElement(u'name', self._reifier(name))
        self._write_reifier(name)
        self._write_iids(name)
        if not is_default_name(name):
            self._write_type(name)
        self._write_scope(name)
        self._writer.dataElement(u'value', name.value)
        write_variant = self._write_variant
        for variant in name.variants:
            write_variant(variant)
        self._writer.endElement(u'name')

    def _write_variant(self, variant):
        """\
        Serializes a variant.
        
        `variant`
            The variant to serialize.
        """
        self._writer.startElement(u'variant', self._reifier(variant))
        self._write_reifier(variant)
        self._write_iids(variant)
        self._write_scope(variant)
        self._write_data(variant)
        self._writer.endElement(u'variant')

    def _write_association(self, assoc):
        """\
        Serializes an association.
        
        `association`
            An association.
        """
        roles = tuple(assoc.roles)
        if not roles:
            return
        self._writer.startElement(u'association', self._reifier(assoc))
        self._write_reifier(assoc)
        self._write_iids(assoc)
        self._write_type(assoc)
        self._write_scope(assoc)
        write_role = self._write_role
        for role in roles:
            write_role(role)
        self._writer.endElement(u'association')

    def _write_role(self, role):
        """\
        Serializes the ``role``.
        
        `role´
            The role to serialize.
        """
        self._writer.startElement(u'role', self._reifier(role))
        self._write_reifier(role)
        self._write_iids(role)
        self._write_type(role)
        self._write_topic_ref(role.player)
        self._writer.endElement(u'role')

    def _write_data(self, datatyped):
        """\
        Serializes the ``value`` and ``datatype`` property of an occurrence or
        variant.

        `datatyped`
            Either an occurrence instance or a variant instance.
        """
        startElement = self._writer.startElement
        endElement = self._writer.endElement
        characters = self._writer.characters
        dt, val = datatyped.datatype, datatyped.value
        if dt == XSD.anyURI:
            self._writer.emptyElement(u'resourceRef', self._href(val))
        else:
            attrs = None
            if dt != XSD.string:
                attrs = {u'datatype': dt}
            startElement(u'resourceData', attrs)
            if dt == XSD.anyType:
                if self._parser is None:
                    self._parser = make_parser()
                    self._parser.setFeature(sax_handler.feature_namespaces, True)
                self._parser.setContentHandler(AnyTypeContentHandler(xmlwriter_as_contenthandler(self._writer)))
                src = InputSource()
                src.setByteStream(io.BytesIO(val))
                self._parser.parse(src)
            else:
                characters(val)
            endElement(u'resourceData', indent=False)

    def _write_iids(self, reifiable):
        """\
        Serializes item identifiers identifiers.
        
        `reifiable`
            The reifiable to fetch the item identifiers from.
        """
        if not self.export_iids:
            return
        self._write_locators(u'itemIdentity', reifiable.iids)

    def _write_locators(self, name, locs):
        """\
        Serializes the specified locators under the specified ``name`` if
        ``locs`` is not empty.
        
        `name`
            The element name of the IRIs
        `locs`
            An iterable of IRIs.
        """
        emptyElement, href = self._writer.emptyElement, self._href
        for loc in locs:
            emptyElement(name, href(loc))

    def _write_reifier_xtm20(self, reifiable):
        """\
        Writes / does nothing.
        """
        pass

    def _write_reifier_xtm21(self, reifiable):
        """\
        Serializes the ``reifier`` property a reifiable statement.
        """
        if reifiable.reifier:
            self._writer.startElement(u'reifier')
            self._write_topic_ref(reifiable.reifier)
            self._writer.endElement(u'reifier')

    def _write_type(self, typed):
        """\
        Serizalizes the ``type`` of a typed Topic Maps construct.
        
        `typed`
            A typed Topic Maps construct (association, role, occurrence, name).
        """
        self._writer.startElement(u'type')
        self._write_topic_ref(typed.type)
        self._writer.endElement(u'type')

    def _write_scope(self, scoped):
        """\
        Serizalizes the ``scope`` of the `scoped` Topic Maps construct.
        
        `scoped`
            The scoped Topic Maps construct (association, occurrence, name, variant)
        """
        write_topic_ref = self._write_topic_ref
        written = False
        for i, theme in enumerate(scoped.scope):
            if not i:
                self._writer.startElement(u'scope')
                written = True
            write_topic_ref(theme)
        if written:
            self._writer.endElement(u'scope')

    def _write_topic_ref_xtm21(self, topic):
        """\
        Writes a ``<topicRef/>`` or ``<subjectIdentifierRef/>`` or 
        ``<subjectLocatorRef/>`` element.
        """
        href = self._href
        iri = one_of(topic.sids)
        if iri:
            self._writer.emptyElement(u'subjectIdentifierRef', href(iri))
        else:
            iri = one_of(topic.slos)
            if iri:
                self._writer.emptyElement(u'subjectLocatorRef', href(iri))
            else:
                iri = one_of(topic.iids) or u'#%s' % topic_id(self._base, topic)
                self._writer.emptyElement(u'topicRef', href(iri))

    def _write_topic_ref_xtm20(self, topic):
        """\
        Writes a ``<topicRef href="#topic-ref"/>`` element.
        """
        href = self._href
        self._writer.emptyElement(u'topicRef', href(u'#%s' % topic_id(self._base, topic)))

    def _reifier_xtm21(self, reifiable):
        """\
        Returns always ``None``.
        """
        return None

    def _reifier_xtm20(self, reifiable):
        """\
        Returns a ``dict```iff the ``reifiable`` Topic Maps construct has a reifier.
        """
        reifier = reifiable.reifier
        if not reifier:
            return None
        return {u'reifier': u'#%s' % topic_id(self._base, reifier)}

    def _href(self, iri):
        """\
        Returns a ``{"href": iri}`` dict.
        """
        return {u'href': iri}


class AnyTypeContentHandler(sax_handler.ContentHandler):
    """\
    ``ContentHandler`` implementation that redirects events to another
    ``ContentHandler``. The ``startDocument`` and ``endDocument`` events are
    ignored.
    """
    def __init__(self, sub_handler):
        sax_handler.ContentHandler.__init__(self)
        self._handler = sub_handler

    def startPrefixMapping(self, prefix, uri):
        self._handler.startPrefixMapping(prefix, uri)

    def endPrefixMapping(self, prefix):
        self._handler.endPrefixMapping(prefix)

    def startElement(self, name, attrs):
        self._handler.startElement(name, attrs)

    def endElement(self, name):
        self._handler.endElement(name)

    def startElementNS(self, name, qname, attrs):
        self._handler.startElementNS(name, qname, attrs)

    def endElementNS(self, name, qname):
        self._handler.endElementNS(name, qname)

    def characters(self, content):
        self._handler.characters(content)

    def ignorableWhitespace(self, content):
        self._handler.ignorableWhitespace(content)

    def processingInstruction(self, target, data):
        self._handler.processingInstruction(target, data)
